---
title: "On Ruby methods"
description: >
  A look on writing Ruby methods that reveal their intent.
categories: posts
tags: [ruby]
---

<details markdown="1" id="table-of-contents">
<summary>
Table of Contents
</summary>

* TOC
{:toc}
</details>

## TL;DR

Commands (change state) return:

  + `self` or failure (`nil` for convenience methods)
  + `nil` or failure
  + yield the result to a block if necessary.

Queries (show state) return:

  + value or default
  + value or null object
  + value or `nil`

Predicates return `true` or `false`.

Interjections should be handle with care.

## Wording

### Procedures

Ruby can encapsulate procedures in methods, and blocks. Unfortunately, that is
not enough to communicate the code's intent. Which can lead to bugs.

A key way to show a procedure's intent is the returned value. When procedures
return `nil` they could easily mean either of these:

- It has no return value.
- There's usually a return value, but not this time.
- It replaces a returned `null` value from a 3rd party eg database.
- Something unexpected happened.

We can help prevent undefined `nil` (those that can't be accounted for in the
test suite) by reasoning the behaviour requested from an object.

Since methods are, by far, more common we'll refer to them the rest of the
note, even when the concepts apply to blocks too.

### Messages

The procedures mentioned above get invoke through messages sent to the objects
that house them.

In Ruby, the messages an object responds to are what define its duck-type, which
is more closely related to interfaces and parametric polymorphism than to subtyping.
Which is a gateway to interfaces, and polymorphism.

Whilst messages can trigger any kind of procedure, we can reveal our code's
intent following a few conventions in both code and tests. More on that below.

#### Functions

When a message triggers a process on one or more arguments (rather than in
collaboration with them) we call it a function. Immutable parts help to keep
functions easy to reason about, but aren't mandatory. eg.

```ruby
1 + 2 #=> 3
```

is a function where `1` and `2` don't change even when combined. Instead, they
produce a new numeric object (`3`). We can reuse them over and over. On the other
hand, functions such as `<<` will always change the state of at least one of the
parts involved.

```ruby
a = "1"
b = "2"
a << b

a #=> "12"
b #=> "2"
```

#### Methods

When a message triggers a process that only affects the object receiving it, or
it's internals, we call it a method. Whilst the returned value may change, it's
always true about the object we are querying.

```ruby
a = ""
a.empty? #=> true

a << "hi"
a.empty? #=> false
```

## Commands

When a message, or method, triggers changes to an attribute, or state,
we may call it a command. Depending on context, a command should return
(in order of preference):

- `self` or fail
- `nil` or fail

When context requires it, we can expose the result of the process via a block.
Then, we can enforce immutability to prevent further changes to the result.

```ruby
def command &block
  fail_with_some_reason # a private method with an exception policy
  block.call @state.freeze if block_given?
  self
end
```

Trade off: Whilst returning `self` allows method chaining, it can lead to bugs
if we are not clear (in code and tests) on what `self` is, and what isn't.
For instance, dependencies can either collaborate in a process or get processed
by it.

On the other hand, when we return `self` rather than `nil` we can use the latter
for convenience singleton methods.

```ruby
class Validation
  ValidationError = Class.new StandardError

  def self.validate obj
    validation = new obj
    validation.validate
  rescue ValidationError # Only rescue exception defined on self
    nil  # assert_nil this
  end

  def initialize obj
    @obj = obj
  end

  def validate
    fail_with_some_reason # a private method with an exception policy
    self
  end

  # more code
end
```

## Queries

Whilst commands focus on changes, queries do so on state. Query methods retrieve
the state of the object we are sending the message to.
They can also retrieve it from collaborators that may get passed as arguments as
long as they don't change the collaborators' actual state.
Thus queries work better when they return:

- queried or default values
- queried value or fail
- queried value or null object
- queried value or `nil`

More often than not, queries are part of the data flow. They are considered
robust when they can gracefully handle mishaps. Yet depending on context
failing may be better than a `null object`. For instance, when handling sensitive
data. Ruby hashes, can be queried like so:

```ruby
hash = Hash.new
hash[:invalid] #=> nil
hash.fetch :invalid #=> KeyError (key not found: :invalid)

other_hash = Hash.new { "default" } # <- that there could be a null object
other_hash[:non_existent] #=> "default"
```

As we've seen, both commands and queries may return `nil` when the 'unhappy'
path is not exceptional. For instance when missing elements.

Even when `nil` can be accounted for on tests, we may have a hard time
differentiating it from those out of unexpected behavior. Returning `nil`
should be our last option.


## Predicates

In Ruby, predicate methods are those that, by convention, end with a question mark
(`#empty?`). Although, ruby considers `nil`, and `false` falsy duck types
predicates are better off returning `true` or `false` to avoid leaking data, in
general, and to prevent bugs originated from leaked data, in particular.


## Interjections

From [Matz himself](https://www.ruby-forum.com/topic/176830#773946){:rel="nofollow noreferrer noopener"}

> The bang (!) does not mean "destructive" nor lack of it mean non-destructive
> either. The bang sign means "the bang version is more dangerous than its non-bang
> counterpart; handle with care".
